/*==========================================================================
 *
 *  Copyright (C) 1995-1996 Microsoft Corporation. All Rights Reserved.
 *
 *  File:   mstream.c
 *  Content:   Illustrates streaming data from a disk MIDI file to a
 *             MIDI stream buffer for playback.
 *
 ***************************************************************************/
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <memory.h>
#include <mmreg.h>

#include "resource.h"
#include "debug.h"
#include "midstuff.h"
#include "mstream.h"

//////////////////////////////////////////////////////////////////////////////
// Lots of global variables

char szAppClass[] = "MStreamWndClass";
char szAppName[]  = "MStream";

char szAppTitle[64];
char szAppCaption[64];
char szOpenFilter[128];
char szOpenDLGTitle[64];
char szProgress[64];
char szTempo[64];
char szVolume[64];

char szTemp[256];
char szDebug[256];
char szFileBuffer[MAX_PATH];
char szFileTitle[MAX_PATH];

HWND    hWndMain, hWndProgText, hWndProg, hWndVolText, hWndVol, hWndTempoText;
HWND    hWndTempo, hWndLoopCheck, hWndPlay, hWndPause, hWndStop;

HINSTANCE   hInst;

BOOL    bFileOpen = FALSE, bPlaying = FALSE, bBuffersPrepared = FALSE;
BOOL    bPaused = FALSE, bLooped = FALSE;
UINT    uMIDIDeviceID = MIDI_MAPPER, uCallbackStatus;
int nTextControlHeight, nCurrentBuffer, nEmptyBuffers;
DWORD   dwBufferTickLength, dwTempoMultiplier, dwCurrentTempo, dwProgressBytes;
DWORD   dwVolumePercent, dwVolCache[NUM_CHANNELS];

HMIDISTRM    hStream;
CONVERTINFO  ciStreamBuffers[NUM_STREAM_BUFFERS];

// Private to this module...
static HANDLE   hBufferReturnEvent;

// From another module...
extern INFILESTATE  ifs;

///////////////////////////////////////////////////////////////////////////////
// Module-scope function declarations

static BOOL InitApp( HINSTANCE );
static BOOL InitInstance( HINSTANCE, int );
static void FreeBuffers( void );

/******************************************************************************
 * WinMain()
 *
 * Entry point for all Windows programs - performs initialization, message loop
 */
int PASCAL WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine, int nCmdShow )
    {
    MSG     msg;

    hInst = hInstance;

    /* Make sure the common controls are loaded for our use */
    InitCommonControls();

// Turn debugging output on or off
#ifdef DEBUG
    DbgInitialize( TRUE );
#else
    DbgInitialize( FALSE );
#endif

    if( !hPrevInstance )
        if( !InitApp( hInstance ))
            {
            ErrorMessageBox( IDS_ERROR_APPINIT, MB_ICONSTOP );
            return( FALSE );
        }

    if( !InitInstance( hInstance, nCmdShow ))
        {
        ErrorMessageBox( IDS_ERROR_INSTANCEINIT, MB_ICONSTOP );
        return( FALSE );
    }

    while( GetMessage((LPMSG)&msg, NULL, 0, 0 ))
        {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
        }

    UnregisterClass( szAppClass, hInstance );
    return( msg.wParam );
    } /* End of WinMain() */


/*****************************************************************************/
/* InitApp()                                     */
/*                                       */
/*   Inits things that only need to be created once for the this application */
/* (like creating the window class).                         */
/*****************************************************************************/
static BOOL InitApp( HINSTANCE hInstance )
    {
    WNDCLASS    wc;

    /* Set up and register a window class */
    wc.style        = CS_HREDRAW | CS_VREDRAW;
    wc.lpszClassName    = szAppClass;
    wc.lpfnWndProc  = (WNDPROC)MainWindowProc;
    wc.cbClsExtra   = 0;
    wc.cbWndExtra   = sizeof( DWORD );
    wc.hInstance    = hInstance;
    wc.hIcon        = LoadIcon( hInstance, MAKEINTRESOURCE( IDI_ICON3 ));
    wc.hCursor      = LoadCursor( NULL, IDC_ARROW );
    wc.hbrBackground    = (HBRUSH)( COLOR_WINDOW );
    wc.lpszMenuName = MAKEINTRESOURCE( IDR_MAINMENU );

    if( !RegisterClass( &wc ))
        {
    ErrorMessageBox( IDS_ERROR_REGISTERCLASS, MB_ICONSTOP );
        return( FALSE );
        }
    return( TRUE );
    } /* End of InitApp() */


/*****************************************************************************/
/* InitInstance()                                */
/*                                       */
/* Performs initialization that must be done for each application instance.  */
/*                                       */
/*****************************************************************************/
static BOOL InitInstance( HINSTANCE hInstance, int nCmdShow )
    {
    HWND    hWnd;
    RECT    crect;
    UINT    uCharsRead;
    MMRESULT    mmrRetVal;

    LoadString( hInstance, IDS_APP_TITLE, szAppTitle, sizeof(szAppTitle));
    LoadString( hInstance, IDS_APP_CAPTION, szAppCaption, sizeof(szAppCaption));
    LoadString( hInstance, IDS_TBTITLE_VOLUME, szVolume, sizeof(szVolume));
    LoadString( hInstance, IDS_TBTITLE_TEMPO, szTempo, sizeof(szTempo));
    LoadString( hInstance, IDS_TBTITLE_PROGRESS,
                            szProgress, sizeof(szProgress));
    LoadString( hInstance, IDS_OPEN_DLGTITLE,
                        szOpenDLGTitle, sizeof(szOpenDLGTitle));
/* This is a little trick designed to allow us to load a common dialog box
 * filter string, which is really a concatentation of several NULL-terminated
 * strings. Note that while is is possible to enter something else into the
 * resource as placeholders for the NULL characters, this has the undesireable
 * effect of forcing us to search-and-replace byte-by-byte and doesn't make it
 * as easy to internationalize our strings...
 */
    memset( szOpenFilter, 0, sizeof(szOpenFilter));
    uCharsRead = LoadString( hInstance, IDS_OPEN_FILTER1,
                szOpenFilter, sizeof(szOpenFilter)) + 1;
    uCharsRead += LoadString( hInstance, IDS_OPEN_FILTER2,
                    &szOpenFilter[uCharsRead],
                    sizeof(szOpenFilter) - uCharsRead ) + 1;
    uCharsRead += LoadString( hInstance, IDS_OPEN_FILTER3,
                    &szOpenFilter[uCharsRead],
                    sizeof(szOpenFilter) - uCharsRead ) + 1;
    LoadString( hInstance, IDS_OPEN_FILTER4,
                    &szOpenFilter[uCharsRead],
                    sizeof(szOpenFilter) - uCharsRead );

    /* Create an application window */
    hWnd = CreateWindow( szAppClass,        /* class name */
            szAppCaption,       /* caption for window */
            WS_OVERLAPPEDWINDOW,    /* style */
            CW_USEDEFAULT,      /* x position */
            CW_USEDEFAULT,      /* y position */
            CW_USEDEFAULT,      /* width */
            CW_USEDEFAULT,      /* height */
            NULL,           /* parent window */
            NULL,           /* menu */
            hInstance,      /* instance */
            NULL );         /* parms */

    if( !hWnd )
        {
    ErrorMessageBox( IDS_ERROR_MAINWNDCREATE, MB_ICONSTOP );
        return( FALSE );
    }

    hWndMain = hWnd;
    GetClientRect( hWndMain, &crect );

    /* Create some controls for things like volume, tempo, progress, etc. */
    if( CreateChildren( crect ))
        return( FALSE );

    // Resize window, now that we know the height of the static text controls
    SetWindowPos( hWnd, NULL, 0, 0,
                2 * BORDER_SPACE_CX + TEMPO_TB_CX + 2 * CONTROL_SPACE_CX
                + VOL_TB_CX + CHECK_CX,
            2 * BORDER_SPACE_CY + nTextControlHeight + TEXT_SPACE_CY
            + CONTROL_SPACE_CY + BUTTON_CY + TEMPO_TB_CY,
            SWP_NOMOVE | SWP_NOZORDER );

    ShowWindow( hWnd, nCmdShow );
    UpdateWindow( hWnd );

    if(( mmrRetVal = midiStreamOpen( &hStream,
                    &uMIDIDeviceID,
                    (DWORD)1, (DWORD)MidiProc,
                    (DWORD)0,
                    CALLBACK_FUNCTION )) != MMSYSERR_NOERROR )
    {
    MidiErrorMessageBox( mmrRetVal );
    return( FALSE );
    }

    return( TRUE );
    } /* End of InitInstance() */


/****************************************************************************/
/* MainWindowProc()                                                         */
/*                                                                          */
/*    Messages for our main window are handled here                         */
/*                                                                          */
/****************************************************************************/
LRESULT CALLBACK MainWindowProc( HWND hWnd, unsigned uMsg,
                        WPARAM wParam, LPARAM lParam )
    {
    LPMINMAXINFO    lpMinMax;
    DWORD   dwCDErr = 0;
    BOOL    bResult = FALSE;
    MMRESULT    mmrRetVal;
    
    switch( uMsg )
        {
    case WM_CREATE:
        hBufferReturnEvent = CreateEvent( NULL, FALSE,
                        FALSE, "Wait For Buffer Return" );
        break;

    case WM_MSTREAM_UPDATEVOLUME:
        SetChannelVolume( wParam, dwVolumePercent );
        break;

    case WM_MSTREAM_PROGRESS:
        /* Set the Progress text */
            wsprintf( szTemp, "%s: %lu bytes", szProgress, dwProgressBytes );
            Static_SetText( hWndProgText, szTemp );
        break;

        case WM_COMMAND:
            switch( LOWORD( wParam ))
                {
                case IDM_FILE_OPEN:
                    {
                    OPENFILENAME    ofn;
    /*
     * Clear out and fill in an OPENFILENAME structure in preparation
     * for creating a common dialog box to open a file.
     */
                    memset( &ofn, 0, sizeof(OPENFILENAME));
                    ofn.lStructSize = sizeof(OPENFILENAME);
                    ofn.hwndOwner   = hWnd;
                    ofn.hInstance   = hInst;
                    ofn.lpstrFilter = szOpenFilter;
                    ofn.nFilterIndex    = 1;
                    szFileBuffer[0] = '\0';
                    ofn.lpstrFile   = szFileBuffer;
                    ofn.nMaxFile    = sizeof(szFileBuffer);
                    ofn.lpstrFileTitle  = szFileTitle;
                    ofn.nMaxFileTitle   = sizeof(szFileTitle);
                    ofn.lpstrDefExt = "MID";
                    ofn.lpstrTitle  = szOpenDLGTitle;
                    ofn.Flags       = OFN_FILEMUSTEXIST;

                    bResult = GetOpenFileName( &ofn ); /* Do the dialog box */

    /*
     *  A return of TRUE indicates that the user did not select a filename.
     * The possible reasons are: Cancel was clicked, or an error occured.
     * If Cancel was clicked, the CommDlgExtendedError() function will not
     * return a valid code.  For anything else, an error code will come back.
     */
                    if( bResult == FALSE )
                        {
                        dwCDErr = CommDlgExtendedError();
                        if( dwCDErr )
                            {
                            /* Handle a common dialog box error */
                            HandleCommDlgError( dwCDErr );
                            }
                        else    /* Clicked Cancel, so finish msg processing */
                            return( 0 );
                        }
                    else
                        {
                        if( bFileOpen )
                            {
// Need to close the previous file before we open a new one.  The best way to
// do this is by faking a menu command, so that we only have the actual code in
// one place and it can easily be changed.
                            SendMessage( hWnd, WM_COMMAND,
                                    MAKEWPARAM( IDM_FILE_CLOSE, 0 ), 0L );
                            }
                    
                        if( StreamBufferSetup())
                            {
                            // Error opening the MIDI file so abort
                // The function already took care of notification
                break;
                            }
                        else
                            {
                            bFileOpen = TRUE;
                            EnableWindow( hWndPlay, TRUE );
                            BuildTitleBarText();
                            }
                        }
                    }
                    break;

                case IDM_FILE_CLOSE:
            SendMessage( hWnd, WM_COMMAND,
                    MAKEWPARAM( IDC_STOP,
                    MSTREAM_STOPF_NOREOPEN ), 0L );
                    BuildTitleBarText();
                    break;

                case IDM_HELP_ABOUT:
                    DialogBox( hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWndMain,
                            (DLGPROC)DLG_About );
                    break;

        case IDM_ACTIONS_PAUSE:
                case IDC_PAUSE:
            if( bPaused )
            midiStreamRestart( hStream );
            else
            midiStreamPause( hStream );
            bPaused = !bPaused;
                    // If we're paused, the title bar will show (Paused)
                    BuildTitleBarText();
            break;

        case IDM_ACTIONS_LOOPED:
        case IDC_LOOPCHECK:
            Button_SetCheck( hWndLoopCheck, !bLooped );
            bLooped = !bLooped;
            break;

        case IDM_ACTIONS_PLAY:
                case IDC_PLAY:
            // Clicking play while playback is paused will un-pause it
            if( bPaused )
            {
            SendMessage( hWnd, WM_COMMAND,
                    MAKEWPARAM( IDC_PAUSE, 0 ), 0L );
            break;
            }
                    // Clicking play while playing will restart from scratch
                    if( bPlaying )
                {
                // Stop the file, allowing it to bve reset so that we
            // can start it over again from the beginning
                SendMessage( hWnd, WM_COMMAND,
                        MAKEWPARAM( IDC_STOP, 0 ), 0L );
            }

                    if( bFileOpen )
                        {
                        // Clear the status of our callback so it will handle
            // MOM_DONE callbacks once more
                        uCallbackStatus = 0;
                        if(( mmrRetVal = midiStreamRestart( hStream ))
                                        != MMSYSERR_NOERROR )
                {
                MidiErrorMessageBox( mmrRetVal );
                break;
                }
                        }
                    else
                        {
                        bPlaying = FALSE;
                        break;
                        }

            bPlaying = TRUE;
            EnableWindow( hWndPause, TRUE );
            EnableWindow( hWndStop, TRUE );
                    break;

                case IDM_ACTIONS_STOP:
                case IDC_STOP:
                    if( bFileOpen || bPlaying
                            || ( uCallbackStatus != STATUS_CALLBACKDEAD ))
            {
            EnableWindow( hWndStop, FALSE );
            EnableWindow( hWndPause, FALSE );
            bPlaying = bPaused = FALSE;
            if( uCallbackStatus != STATUS_CALLBACKDEAD && uCallbackStatus != STATUS_WAITINGFOREND )
                uCallbackStatus = STATUS_KILLCALLBACK;

                        if(( mmrRetVal = midiStreamStop( hStream ))
                                        != MMSYSERR_NOERROR )
                {
                MidiErrorMessageBox( mmrRetVal );
                break;
                }
                        if(( mmrRetVal = midiOutReset( (HMIDIOUT)hStream ))
                                        != MMSYSERR_NOERROR )
                {
                MidiErrorMessageBox( mmrRetVal );
                break;
                }
// Wait for the callback thread to release this thread, which it will do by
// calling SetEvent() once all buffers are returned to it
                        if( WaitForSingleObject( hBufferReturnEvent,
                                    DEBUG_CALLBACK_TIMEOUT )
                                            == WAIT_TIMEOUT )
                {
// Note, this is a risky move because the callback may be genuinely busy, but
// when we're debugging, it's safer and faster than freezing the application,
// which leaves the MIDI device locked up and forces a system reset...
                DebugPrint( "Timed out waiting for MIDI callback" );
                uCallbackStatus = STATUS_CALLBACKDEAD;
                }
                        }

            if( uCallbackStatus == STATUS_CALLBACKDEAD )
            {
            uCallbackStatus = 0;
            if( bFileOpen )
                {
                ConverterCleanup();
                FreeBuffers();
                if( hStream )
                {
                if(( mmrRetVal = midiStreamClose( hStream ))
                                != MMSYSERR_NOERROR )
                    {
                    MidiErrorMessageBox( mmrRetVal );
                    }
                hStream = NULL;
                }

                EnableWindow( hWndPlay, FALSE );
                bFileOpen = FALSE;
                }
                        
                        if(!( HIWORD(wParam) & MSTREAM_STOPF_NOREOPEN ))
                            {
                            if( StreamBufferSetup())
                {
                // Error setting up for MIDI file
                // Notification is already taken care of...
                break;
                }
                            else
                                {
                                bFileOpen = TRUE;
                                EnableWindow( hWndPlay, TRUE );
                                }
                            }
            BuildTitleBarText();    // Update the title bar
                        }
                    break;

                case IDM_FILE_EXIT:
                    DestroyWindow( hWnd );
                    break;
                }
            break;

    case WM_INITMENU:
        if( bFileOpen )
        {
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_PLAY,
                                            MF_BYCOMMAND | MF_ENABLED );
        EnableMenuItem( GetMenu( hWnd ), IDM_FILE_CLOSE,
                                            MF_BYCOMMAND | MF_ENABLED );
        }
        else
        {
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_PLAY,
                                            MF_BYCOMMAND | MF_GRAYED );
        EnableMenuItem( GetMenu( hWnd ), IDM_FILE_CLOSE,
                                            MF_BYCOMMAND | MF_GRAYED );
        }
        if( bLooped )
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_LOOPED,
                                            MF_BYCOMMAND | MF_ENABLED );
        else
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_LOOPED,
                                            MF_BYCOMMAND | MF_GRAYED );
        if( bPlaying )
        {
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_PAUSE,
                                            MF_BYCOMMAND | MF_ENABLED );
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_STOP,
                                            MF_BYCOMMAND | MF_ENABLED );
        }
        else
        {
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_PAUSE,
                                            MF_BYCOMMAND | MF_GRAYED );
        EnableMenuItem( GetMenu( hWnd ), IDM_ACTIONS_STOP,
                                            MF_BYCOMMAND | MF_GRAYED );
        }
        break;

        case WM_GETMINMAXINFO:
    /*
     * We know exactly how big this window should be, and it's sort of a
     * little pop-up control panel, so we can disable window sizing by
     * forcing all the minimum and maximum sizes to be the calculated size.
     */
            lpMinMax = (LPMINMAXINFO)lParam;

            lpMinMax->ptMaxSize.x = 2*CONTROL_SPACE_CX + 2*BORDER_SPACE_CX
                                    + CHECK_CX + TEMPO_TB_CX + VOL_TB_CX
                                    + 2*GetSystemMetrics( SM_CXBORDER );
            lpMinMax->ptMaxSize.y = 2*(BORDER_SPACE_CY
                                    + GetSystemMetrics( SM_CYBORDER ))
                                    + TEXT_SPACE_CY + nTextControlHeight
                                    + TEMPO_TB_CY + BUTTON_CY
                                    + CONTROL_SPACE_CY
                                    + GetSystemMetrics( SM_CYMENU )
                                    + GetSystemMetrics( SM_CYCAPTION );

            lpMinMax->ptMinTrackSize.x = lpMinMax->ptMaxTrackSize.x
                                        = lpMinMax->ptMaxSize.x;

            lpMinMax->ptMinTrackSize.y = lpMinMax->ptMaxTrackSize.y
                                        = lpMinMax->ptMaxSize.y;
            break;

        case WM_HSCROLL:
            if(((HWND)lParam == hWndTempo) && bFileOpen )
                {
                HandleTempoScroll( (int)LOWORD(wParam), (int)HIWORD(wParam));
                }
            else if(((HWND)lParam == hWndVol) && bFileOpen )
                {
                HandleVolScroll( (int)LOWORD(wParam), (int)HIWORD(wParam));
                }
            break;

    case WM_ENDSESSION:
        // If the sesson is ending, we need to do our WM_DESTROY processing
        if (!wParam) break;
        // NOTE!!! we are falling through to the WM_CLOSE processing.
        case WM_DESTROY:
            // Stop anything that might be playing and send a flag which will
        // tell the code not to automatically reload the file for replay.
            if( hStream )
        SendMessage( hWnd, WM_COMMAND,
                          MAKEWPARAM( IDC_STOP, MSTREAM_STOPF_NOREOPEN ), 0 );

        FreeBuffers();

        if( hStream )
        {
        if(( mmrRetVal = midiStreamClose( hStream ))
                                != MMSYSERR_NOERROR )
            {
            MidiErrorMessageBox( mmrRetVal );
            }
        hStream = NULL;
        }

            CloseHandle( hBufferReturnEvent );
            
            PostQuitMessage( 0 );
            break;

        default:
            return DefWindowProc( hWnd, uMsg, wParam, lParam );
        }
    return 0L;
    } /* MainWindowProc */


/*****************************************************************************/
/* DLG_About()                                                               */
/*                                                                           */
/*   Dialog procedure for the Help...About... box which simply pops up a     */
/* little copyright message and brief program description.                   */
/*                                                                           */
/*****************************************************************************/
BOOL CALLBACK DLG_About( HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam )
    {
    switch( msg )
        {
        case WM_INITDIALOG:
            break;

        case WM_COMMAND:
            switch( LOWORD(wParam))
                {
                case IDOK:
                    EndDialog( hDlg, FALSE );
                    return( TRUE );

                default:
                    break;
                }
            break;

        default:
            return( FALSE );
        }

    return( FALSE );
    }


/*****************************************************************************/
/* FreeBuffers()                                                             */
/*                                                                           */
/*   This function unprepares and frees all our buffers -- something we must */
/* do to work around a bug in MMYSYSTEM that prevents a device from playing  */
/* back properly unless it is closed and reopened after each stop.           */
/*****************************************************************************/
void FreeBuffers( void )
    {
    DWORD   idx;
    MMRESULT    mmrRetVal;

    if( bBuffersPrepared )
    {
    for( idx = 0; idx < NUM_STREAM_BUFFERS; idx++ )
        if(( mmrRetVal = midiOutUnprepareHeader( (HMIDIOUT)hStream,
                &ciStreamBuffers[idx].mhBuffer,
                sizeof(MIDIHDR)))
                        != MMSYSERR_NOERROR )
        {
        MidiErrorMessageBox( mmrRetVal );
        }
        bBuffersPrepared = FALSE;
        }
    // Free our stream buffers...
    for( idx = 0; idx < NUM_STREAM_BUFFERS; idx++ )
        if( ciStreamBuffers[idx].mhBuffer.lpData )
        {
        GlobalFreePtr( ciStreamBuffers[idx].mhBuffer.lpData );
        ciStreamBuffers[idx].mhBuffer.lpData = NULL;
        }
    }


/*****************************************************************************/
/* StreamBufferSetup()                                                       */
/*                                                                           */
/*    This function uses the filename stored in the global character array to*/
/* open a MIDI file. Then it goes about converting at least the first part of*/
/* that file into a midiStream buffer for playback.                          */
/*****************************************************************************/
BOOL StreamBufferSetup( void )
    {
    int     nChkErr;
    BOOL    bFoundEnd = FALSE;
    DWORD   dwConvertFlag, idx;

    MMRESULT        mmrRetVal;
    MIDIPROPTIMEDIV mptd;

    if( !hStream )
    if(( mmrRetVal = midiStreamOpen( &hStream,
                    &uMIDIDeviceID,
                    (DWORD)1, (DWORD)MidiProc,
                    (DWORD)0,
                    CALLBACK_FUNCTION )) != MMSYSERR_NOERROR )
        {
        MidiErrorMessageBox( mmrRetVal );
        return( TRUE );
        }

    for( idx = 0; idx < NUM_STREAM_BUFFERS; idx++ )
    {
    ciStreamBuffers[idx].mhBuffer.dwBufferLength = OUT_BUFFER_SIZE;
    if(( ciStreamBuffers[idx].mhBuffer.lpData
        = GlobalAllocPtr( GHND, OUT_BUFFER_SIZE )) == NULL )
        {
        // Buffers we already allocated will be killed by WM_DESTROY
        // after we fail on the create by returning with -1
        return( -1 );
        }
    }
    if( ConverterInit( szFileBuffer ))
    return( TRUE );

    // Initialize the volume cache array to some pre-defined value
    for( idx = 0; idx < NUM_CHANNELS; idx++ )
    dwVolCache[idx] = VOL_CACHE_INIT;

    mptd.cbStruct = sizeof(mptd);
    mptd.dwTimeDiv = ifs.dwTimeDivision;
    if(( mmrRetVal = midiStreamProperty( hStream, (LPBYTE)&mptd,
                        MIDIPROP_SET | MIDIPROP_TIMEDIV ))
                                != MMSYSERR_NOERROR )
    {
    MidiErrorMessageBox( mmrRetVal );
    ConverterCleanup();
    return( TRUE );
    }

    nEmptyBuffers = 0;
    dwConvertFlag = CONVERTF_RESET;

    for( nCurrentBuffer = 0; nCurrentBuffer < NUM_STREAM_BUFFERS;
                                    nCurrentBuffer++ )
    {
    // Tell the converter to convert up to one entire buffer's length of output
    // data. Also, set a flag so it knows to reset any saved state variables it
    // may keep from call to call.
    ciStreamBuffers[nCurrentBuffer].dwStartOffset = 0;
    ciStreamBuffers[nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE;
    ciStreamBuffers[nCurrentBuffer].tkStart = 0;
        ciStreamBuffers[nCurrentBuffer].bTimesUp = FALSE;

    if(( nChkErr = ConvertToBuffer( dwConvertFlag,
                    &ciStreamBuffers[nCurrentBuffer] ))
                                != CONVERTERR_NOERROR )
            {
        if( nChkErr == CONVERTERR_DONE )
        {
        bFoundEnd = TRUE;
        }
        else
        {
        DebugPrint( "Initial conversion pass failed" );
        ConverterCleanup();
        return( TRUE );
        }
        }
    ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded
                = ciStreamBuffers[nCurrentBuffer].dwBytesRecorded;

    if( !bBuffersPrepared )
        if(( mmrRetVal = midiOutPrepareHeader( (HMIDIOUT)hStream,
                    &ciStreamBuffers[nCurrentBuffer].mhBuffer,
                    sizeof(MIDIHDR))) != MMSYSERR_NOERROR )
        {
        MidiErrorMessageBox( mmrRetVal );
        ConverterCleanup();
        return( TRUE );
        }
    if(( mmrRetVal = midiStreamOut( hStream,
                    &ciStreamBuffers[nCurrentBuffer].mhBuffer,
                    sizeof(MIDIHDR))) != MMSYSERR_NOERROR )
        {
        MidiErrorMessageBox( mmrRetVal );
        break;
        }
    dwConvertFlag = 0;

    if( bFoundEnd )
        break;
    }

    bBuffersPrepared = TRUE;
    nCurrentBuffer = 0;
    UpdateFromControls();
    return( FALSE );
    }


/*****************************************************************************/
/* MidiProc()                                                                */
/*                                                                           */
/*   This is the callback handler which continually refills MIDI data buffers*/
/* as they're returned to us from the audio subsystem.                       */
/*****************************************************************************/
void CALLBACK MidiProc( HMIDIIN hMidi, UINT uMsg, DWORD dwInstance,
            DWORD dwParam1, DWORD dwParam2 )
    {
    static int  nWaitingBuffers = 0;
    MIDIEVENT   *pme;
    MIDIHDR *pmh;

    MMRESULT    mmrRetVal;
    int     nChkErr;


    switch( uMsg )
    {
    case MOM_DONE:
            if( uCallbackStatus == STATUS_CALLBACKDEAD )
                return;

        nEmptyBuffers++;

        if( uCallbackStatus == STATUS_WAITINGFOREND )
        {
        if( nEmptyBuffers < NUM_STREAM_BUFFERS )
            {
            return;
            }
        else
            {
            uCallbackStatus = STATUS_CALLBACKDEAD;
            PostMessage( hWndMain, WM_COMMAND,
                    MAKEWPARAM( IDC_STOP, 0 ), 0L );
            SetEvent( hBufferReturnEvent );
            return;
            }
        }

    // This flag is set whenever the callback is waiting for all buffers to
    // come back.
            if( uCallbackStatus == STATUS_KILLCALLBACK )
        {
        // Count NUM_STREAM_BUFFERS-1 being returned for the last time
        if( nEmptyBuffers < NUM_STREAM_BUFFERS )
                    {
                    return;
                    }
        // Then send a stop message when we get the last buffer back...
        else
            {
            // Change the status to callback dead
            uCallbackStatus = STATUS_CALLBACKDEAD;
            SetEvent( hBufferReturnEvent );
            return;
            }
                }

            dwProgressBytes
                += ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded;
        PostMessage( hWndMain, WM_MSTREAM_PROGRESS, 0L, 0L );

///////////////////////////////////////////////////////////////////////////////
// Fill an available buffer with audio data again...

        if( bPlaying && nEmptyBuffers )
        {
        ciStreamBuffers[nCurrentBuffer].dwStartOffset = 0;
        ciStreamBuffers[nCurrentBuffer].dwMaxLength = OUT_BUFFER_SIZE;
        ciStreamBuffers[nCurrentBuffer].tkStart = 0;
        ciStreamBuffers[nCurrentBuffer].dwBytesRecorded = 0;
        ciStreamBuffers[nCurrentBuffer].bTimesUp = FALSE;

        if(( nChkErr = ConvertToBuffer( 0,
                        &ciStreamBuffers[nCurrentBuffer] ))
                                != CONVERTERR_NOERROR )
                {
            if( nChkErr == CONVERTERR_DONE )
            {
            // Don't include this one in the count
            nWaitingBuffers = NUM_STREAM_BUFFERS - 1;
            uCallbackStatus = STATUS_WAITINGFOREND;
            return;
            }
            else
            {
            DebugPrint( "MidiProc() conversion pass failed!" );
            ConverterCleanup();
            return;
            }
            }

        ciStreamBuffers[nCurrentBuffer].mhBuffer.dwBytesRecorded
                = ciStreamBuffers[nCurrentBuffer].dwBytesRecorded;

        if(( mmrRetVal = midiStreamOut( hStream,
                    &ciStreamBuffers[nCurrentBuffer].mhBuffer,
                    sizeof(MIDIHDR))) != MMSYSERR_NOERROR )
            {
            MidiErrorMessageBox( mmrRetVal );
            ConverterCleanup();
            return;
            }
        nCurrentBuffer = ( nCurrentBuffer + 1 ) % NUM_STREAM_BUFFERS;
        nEmptyBuffers--;
        }

        break;

    case MOM_POSITIONCB:
        pmh = (MIDIHDR *)dwParam1;
        pme = (MIDIEVENT *)(pmh->lpData + pmh->dwOffset);
        if( MIDIEVENT_TYPE( pme->dwEvent ) == MIDI_CTRLCHANGE )
        {
        if( MIDIEVENT_DATA1( pme->dwEvent ) == MIDICTRL_VOLUME_LSB )
            {
            DebugPrint( "Got an LSB volume event" );
            break;
            }
        if( MIDIEVENT_DATA1( pme->dwEvent ) != MIDICTRL_VOLUME )
            break;

        // Mask off the channel number and cache the volume data byte
        dwVolCache[ MIDIEVENT_CHANNEL( pme->dwEvent )]
                    = MIDIEVENT_VOLUME( pme->dwEvent );
        // Post a message so that the main program knows to counteract
        // the effects of the volume event in the stream with its own
        // generated event which reflects the proper trackbar position.
        PostMessage( hWndMain, WM_MSTREAM_UPDATEVOLUME,
                MIDIEVENT_CHANNEL( pme->dwEvent ), 0L );
        }
        break;

    default:
        break;
    }

    return;
    }
